"""
特征编码案例：类别特征编码
演示各种类别特征编码方法及其应用场景
"""

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import LabelEncoder, OneHotEncoder, OrdinalEncoder
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
import seaborn as sns

# 设置中文显示
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False

print("=" * 80)
print("特征编码案例：类别特征编码")
print("=" * 80)

# 1. 创建示例数据
print("\n1. 创建示例数据集")
print("=" * 80)

data = {
    '城市': ['北京', '上海', '广州', '深圳', '北京', '上海', '广州', '深圳', '北京', '上海'],
    '学历': ['本科', '硕士', '博士', '本科', '硕士', '博士', '本科', '硕士', '博士', '本科'],
    '职位等级': ['初级', '中级', '高级', '初级', '中级', '高级', '中级', '高级', '初级', '中级'],
    '薪资': [8000, 15000, 25000, 9000, 16000, 28000, 10000, 18000, 8500, 14000]
}

df = pd.DataFrame(data)
print("\n原始数据：")
print(df)
print(f"\n数据形状：{df.shape}")
print(f"\n数据类型：\n{df.dtypes}")

# 2. 标签编码 (Label Encoding)
print("\n" + "=" * 80)
print("2. 标签编码 (Label Encoding)")
print("=" * 80)
print("原理：将类别转换为数字标签 (0, 1, 2, ...)")
print("适用场景：目标变量编码，或有序类别变量")
print("注意：会引入顺序关系，不适合无序类别变量")

# 对城市进行标签编码
le_city = LabelEncoder()
df['城市_标签编码'] = le_city.fit_transform(df['城市'])

print("\n城市的标签编码：")
print(df[['城市', '城市_标签编码']])
print(f"\n编码映射：{dict(zip(le_city.classes_, le_city.transform(le_city.classes_)))}")

# 3. 序号编码 (Ordinal Encoding)
print("\n" + "=" * 80)
print("3. 序号编码 (Ordinal Encoding)")
print("=" * 80)
print("原理：为有序类别变量指定顺序")
print("适用场景：有明确顺序关系的类别变量（如学历、职位等级）")

# 定义顺序
education_order = ['本科', '硕士', '博士']
position_order = ['初级', '中级', '高级']

# 使用OrdinalEncoder
ordinal_encoder = OrdinalEncoder(categories=[education_order])
df['学历_序号编码'] = ordinal_encoder.fit_transform(df[['学历']])

# 手动映射职位等级
position_mapping = {'初级': 0, '中级': 1, '高级': 2}
df['职位等级_序号编码'] = df['职位等级'].map(position_mapping)

print("\n学历的序号编码（本科<硕士<博士）：")
print(df[['学历', '学历_序号编码']])
print("\n职位等级的序号编码（初级<中级<高级）：")
print(df[['职位等级', '职位等级_序号编码']])

# 4. 独热编码 (One-Hot Encoding)
print("\n" + "=" * 80)
print("4. 独热编码 (One-Hot Encoding)")
print("=" * 80)
print("原理：将类别转换为二进制向量，每个类别一列")
print("适用场景：无序类别变量，避免引入错误的顺序关系")
print("注意：会增加特征维度，类别过多时慎用")

# 方法1：使用pandas的get_dummies
df_onehot_pd = pd.get_dummies(df['城市'], prefix='城市')
print("\n使用pandas进行独热编码：")
print(df_onehot_pd)

# 方法2：使用sklearn的OneHotEncoder
onehot_encoder = OneHotEncoder(sparse_output=False)
city_onehot = onehot_encoder.fit_transform(df[['城市']])
city_onehot_df = pd.DataFrame(
    city_onehot,
    columns=[f'城市_{cat}' for cat in onehot_encoder.categories_[0]]
)
print("\n使用sklearn进行独热编码：")
print(city_onehot_df)

# 合并到原数据
df_with_onehot = pd.concat([df, city_onehot_df], axis=1)
print("\n合并独热编码后的数据：")
print(df_with_onehot)

# 5. 二进制编码 (Binary Encoding)
print("\n" + "=" * 80)
print("5. 二进制编码 (Binary Encoding)")
print("=" * 80)
print("原理：先标签编码，再转换为二进制")
print("适用场景：类别数量较多时，比独热编码更节省空间")

# 手动实现二进制编码
def binary_encode(series):
    """二进制编码"""
    le = LabelEncoder()
    labels = le.fit_transform(series)
    
    # 计算需要的二进制位数
    n_bits = int(np.ceil(np.log2(len(le.classes_))))
    
    # 转换为二进制
    binary_encoded = np.array([list(map(int, format(label, f'0{n_bits}b'))) 
                               for label in labels])
    
    # 创建DataFrame
    columns = [f'{series.name}_bin_{i}' for i in range(n_bits)]
    return pd.DataFrame(binary_encoded, columns=columns)

city_binary = binary_encode(df['城市'])
print("\n城市的二进制编码：")
print(pd.concat([df['城市'], city_binary], axis=1))

# 6. 频率编码 (Frequency Encoding)
print("\n" + "=" * 80)
print("6. 频率编码 (Frequency Encoding)")
print("=" * 80)
print("原理：用类别出现的频率替换类别")
print("适用场景：类别的频率信息对预测有帮助")

# 计算频率
city_freq = df['城市'].value_counts() / len(df)
df['城市_频率编码'] = df['城市'].map(city_freq)

print("\n城市的频率编码：")
print(df[['城市', '城市_频率编码']])

# 7. 目标编码 (Target Encoding)
print("\n" + "=" * 80)
print("7. 目标编码 (Target Encoding)")
print("=" * 80)
print("原理：用目标变量的统计量（均值、中位数等）替换类别")
print("适用场景：类别与目标变量有较强关联")
print("注意：容易过拟合，需要使用交叉验证")

# 计算每个城市的平均薪资
city_mean_salary = df.groupby('城市')['薪资'].mean()
df['城市_目标编码'] = df['城市'].map(city_mean_salary)

print("\n城市的目标编码（平均薪资）：")
print(df[['城市', '薪资', '城市_目标编码']])

# 8. 对比不同编码方法
print("\n" + "=" * 80)
print("8. 不同编码方法对比")
print("=" * 80)

comparison_df = pd.DataFrame({
    '原始': df['城市'],
    '标签编码': df['城市_标签编码'],
    '频率编码': df['城市_频率编码'].round(2),
    '目标编码': df['城市_目标编码']
})
print(comparison_df)

# 9. 实际应用：编码方法对模型的影响
print("\n" + "=" * 80)
print("9. 编码方法对模型性能的影响")
print("=" * 80)

# 创建更大的数据集用于测试
np.random.seed(42)
n_samples = 1000

cities = ['北京', '上海', '广州', '深圳', '杭州']
educations = ['本科', '硕士', '博士']
positions = ['初级', '中级', '高级']

large_data = {
    '城市': np.random.choice(cities, n_samples),
    '学历': np.random.choice(educations, n_samples),
    '职位等级': np.random.choice(positions, n_samples)
}

# 生成目标变量（基于特征的组合）
df_large = pd.DataFrame(large_data)
education_score = df_large['学历'].map({'本科': 1, '硕士': 2, '博士': 3})
position_score = df_large['职位等级'].map({'初级': 1, '中级': 2, '高级': 3})
city_score = df_large['城市'].map({'北京': 1.2, '上海': 1.2, '广州': 1.0, '深圳': 1.1, '杭州': 1.0})

df_large['薪资等级'] = ((education_score + position_score) * city_score + 
                       np.random.normal(0, 0.5, n_samples)).astype(int)
df_large['薪资等级'] = df_large['薪资等级'].clip(0, 10)

print(f"数据集大小：{df_large.shape}")
print(f"目标变量分布：\n{df_large['薪资等级'].value_counts().sort_index()}")

# 准备特征和目标
X = df_large[['城市', '学历', '职位等级']]
y = df_large['薪资等级']

# 分割数据
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# 测试不同编码方法
results = {}

# 方法1：标签编码
X_train_label = X_train.copy()
X_test_label = X_test.copy()
for col in X_train.columns:
    le = LabelEncoder()
    X_train_label[col] = le.fit_transform(X_train[col])
    X_test_label[col] = le.transform(X_test[col])

rf_label = RandomForestClassifier(n_estimators=100, random_state=42)
rf_label.fit(X_train_label, y_train)
results['标签编码'] = accuracy_score(y_test, rf_label.predict(X_test_label))

# 方法2：独热编码
X_train_onehot = pd.get_dummies(X_train)
X_test_onehot = pd.get_dummies(X_test)

# 确保训练集和测试集有相同的列
X_test_onehot = X_test_onehot.reindex(columns=X_train_onehot.columns, fill_value=0)

rf_onehot = RandomForestClassifier(n_estimators=100, random_state=42)
rf_onehot.fit(X_train_onehot, y_train)
results['独热编码'] = accuracy_score(y_test, rf_onehot.predict(X_test_onehot))

# 方法3：序号编码
X_train_ordinal = X_train.copy()
X_test_ordinal = X_test.copy()

education_map = {'本科': 0, '硕士': 1, '博士': 2}
position_map = {'初级': 0, '中级': 1, '高级': 2}
city_map = {city: i for i, city in enumerate(cities)}

X_train_ordinal['学历'] = X_train_ordinal['学历'].map(education_map)
X_train_ordinal['职位等级'] = X_train_ordinal['职位等级'].map(position_map)
X_train_ordinal['城市'] = X_train_ordinal['城市'].map(city_map)

X_test_ordinal['学历'] = X_test_ordinal['学历'].map(education_map)
X_test_ordinal['职位等级'] = X_test_ordinal['职位等级'].map(position_map)
X_test_ordinal['城市'] = X_test_ordinal['城市'].map(city_map)

rf_ordinal = RandomForestClassifier(n_estimators=100, random_state=42)
rf_ordinal.fit(X_train_ordinal, y_train)
results['序号编码'] = accuracy_score(y_test, rf_ordinal.predict(X_test_ordinal))

# 打印结果
print("\n不同编码方法的模型准确率：")
for method, acc in results.items():
    print(f"{method}: {acc:.4f}")

# 10. 可视化
print("\n" + "=" * 80)
print("10. 可视化编码方法对比")
print("=" * 80)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 准确率对比
methods = list(results.keys())
accuracies = list(results.values())
axes[0].bar(methods, accuracies, color=['#1f77b4', '#ff7f0e', '#2ca02c'])
axes[0].set_ylabel('准确率')
axes[0].set_title('不同编码方法的模型准确率对比')
axes[0].set_ylim([0, 1])
axes[0].grid(True, alpha=0.3, axis='y')
for i, v in enumerate(accuracies):
    axes[0].text(i, v + 0.01, f'{v:.4f}', ha='center', va='bottom')

# 特征数量对比
feature_counts = {
    '标签编码': X_train_label.shape[1],
    '独热编码': X_train_onehot.shape[1],
    '序号编码': X_train_ordinal.shape[1]
}
axes[1].bar(feature_counts.keys(), feature_counts.values(), 
            color=['#1f77b4', '#ff7f0e', '#2ca02c'])
axes[1].set_ylabel('特征数量')
axes[1].set_title('不同编码方法的特征数量对比')
axes[1].grid(True, alpha=0.3, axis='y')
for i, (k, v) in enumerate(feature_counts.items()):
    axes[1].text(i, v + 0.5, str(v), ha='center', va='bottom')

plt.tight_layout()
plt.savefig('/Users/binming/Desktop/CodeBase/python/机器学习/特征工程/3-特征编码/编码方法对比.png', 
            dpi=300, bbox_inches='tight')
print("可视化图表已保存")

# 11. 编码方法选择指南
print("\n" + "=" * 80)
print("11. 编码方法选择指南")
print("=" * 80)
print("""
┌─────────────────┬──────────────────────┬─────────────────────┬──────────────────┐
│   编码方法      │      适用场景        │        优点         │      缺点        │
├─────────────────┼──────────────────────┼─────────────────────┼──────────────────┤
│ 标签编码        │ 目标变量、树模型     │ 简单、不增加维度    │ 引入顺序关系     │
│ Label Encoding  │                      │                     │                  │
├─────────────────┼──────────────────────┼─────────────────────┼──────────────────┤
│ 序号编码        │ 有序类别变量         │ 保留顺序信息        │ 需要定义顺序     │
│ Ordinal Encoding│ (学历、等级等)       │                     │                  │
├─────────────────┼──────────────────────┼─────────────────────┼──────────────────┤
│ 独热编码        │ 无序类别变量         │ 不引入顺序关系      │ 增加维度、稀疏   │
│ One-Hot Encoding│ 线性模型             │                     │                  │
├─────────────────┼──────────────────────┼─────────────────────┼──────────────────┤
│ 二进制编码      │ 高基数类别变量       │ 比独热编码节省空间  │ 实现复杂         │
│ Binary Encoding │                      │                     │                  │
├─────────────────┼──────────────────────┼─────────────────────┼──────────────────┤
│ 频率编码        │ 频率信息重要         │ 简单、保留信息      │ 可能碰撞         │
│ Frequency       │                      │                     │                  │
├─────────────────┼──────────────────────┼─────────────────────┼──────────────────┤
│ 目标编码        │ 类别与目标相关       │ 强大、信息丰富      │ 容易过拟合       │
│ Target Encoding │                      │                     │                  │
└─────────────────┴──────────────────────┴─────────────────────┴──────────────────┘

选择建议：
1. 树模型（随机森林、XGBoost）：标签编码或序号编码
2. 线性模型（逻辑回归、SVM）：独热编码
3. 神经网络：独热编码或嵌入层
4. 高基数类别（>50个类别）：二进制编码、哈希编码或目标编码
5. 有序类别：序号编码
6. 无序类别：独热编码
""")

print("\n" + "=" * 80)
print("案例完成！")
print("=" * 80)
